using System;
using System.Collections.Generic;
using System.Text;
using System.Xml;
using System.Net;
using System.IO;
using System.Text.RegularExpressions;
using System.Security.Cryptography;
using System.Diagnostics;
using System.Threading;

namespace CodePlex.Resourceful.Amazon.S3
{
    public class S3Connection
    {
        private readonly List<ACLPrincipal> _knownPrincipals;
        private readonly S3Client _client;
        private ACLPrincipal _owner;
        private readonly RequestStyle _requestStylePreference;

        public S3Connection(string accessKeyID, string secretAccessKey, bool useSSL, RequestStyle preference)
        {
            if (string.IsNullOrEmpty(accessKeyID))
            {
                throw new ArgumentException("accessKeyID cannot be blank");
            }

            if (string.IsNullOrEmpty(secretAccessKey))
            {
                throw new ArgumentException("secretAccessKey cannot be blank");
            }

            _client = new S3Client(accessKeyID, secretAccessKey, useSSL);

            _knownPrincipals = new List<ACLPrincipal>();

            foreach (ACLPrincipal known in S3Constants.Principals.All)
            {
                 _knownPrincipals.Add(known);
            }

            _requestStylePreference = preference;
        }

        



        // client-like forwards

        public Tracer Tracer
        {
            get
            {
                return _client.Tracer;
            }
        }

        public string AccessKeyID
        {
            get
            {
                return _client.AccessKeyID;
            }
        }

        public bool IsSSL
        {
            get
            {
                return _client.IsSSL;
            }
        }

        public bool IsBucketValid(string bucket, BucketNaming level)
        {
            return _client.IsBucketValid(bucket, level);
        }

        public bool IsKeyValid(string key)
        {
            return _client.IsKeyValid(key);
        }

       




  
        // simple properties

        public DateTime AmazonUtcNow
        {
            get
            {
                string dateHeader = _client.HeadService().Headers[HttpHeader.Date];
                return DateTime.SpecifyKind(DateTime.Parse(dateHeader).ToUniversalTime(), DateTimeKind.Utc);
            }
        }

        public RequestStyle RequestStylePreference
        {
            get
            {
                return _requestStylePreference;
            }
        }

        public ACLPrincipal Owner
        {
            get
            {
                if (_owner == null)
                {
                    _owner = this.ListBuckets().Owner;
                }
                return _owner;
            }
        }

        public bool IsValid
        {
            get
            {
                try
                {
                    this.ListBuckets();
                    return true;
                }
                catch
                {
                    return false;
                }
            }
        }

 






        // misc

        public string GetSignedUrl( string bucket, string key, TimeSpan expirationTimeFromNow)
        {
            DateTime amazonExpires = this.AmazonUtcNow.Add(expirationTimeFromNow);
            Datacenter location = this.GetDatacenter(bucket);
            RequestStyle style = location == Datacenter.EU ? RequestStyle.VHost : RequestStyle.Path;

            return _client.GetSignedUrl(style, bucket, key, amazonExpires);
        }






        // operations on service

        public ListBucketsResult ListBuckets()
        {
            ClientResponse response = _client.GetService();

            if (response.HttpStatus.Equals(HttpStatus.OK))
            {
                ListBucketsResult rt = ListBucketsResult.Parse(response.AsXmlReader());
                this.PresentACLPrincipal(rt.Owner);
                return rt;
            }

            throw this.Unexpected(response);
        }





        // operations on buckets

        public void CreateBucket(string bucket)
        {
            this.CreateBucket(bucket, Datacenter.US);
        }

        public void CreateBucket(string bucket, Datacenter location)
        {
            if (location == Datacenter.EU && !this.IsBucketValid(bucket, BucketNaming.ValidDNS))
            {
                throw new ArgumentException("Bucket name is not valid for vhost-style requests.", "bucket");
            }

            ClientResponse response = _client.PutBucket(bucket, location);

            if (response.HttpStatus.Equals(HttpStatus.OK))
            {
                // expect nothing but headers
                return;
            }

            throw this.Unexpected(response);
        }

        public void DeleteBucket(string bucket)
        {
            ClientResponse response = this.HandleRedirects(bucket,null,delegate(S3Client item0, RequestStyle item1)
            {
                return item0.DeleteBucket(item1, bucket);
            });

            if (response.HttpStatus.Equals(HttpStatus.NoContent))
            {
                // expected
                return;
            }
           
            throw this.Unexpected(response);
        }

        public ReadOnlyList<Key,string> ListBucket(string bucket)
        {
            return this.ListBucket(bucket, null);
        }

        public ReadOnlyList<Key,string> ListBucket(string bucket, string prefix)
        {
            return this.ListBucket(bucket, prefix, null).Keys;
        }

        public ListBucketResult ListBucket(string bucket, string prefix, string delimiter)
        {
            ListBucketResult rt = this.InternalListBucket(bucket, prefix, delimiter);

            foreach (Key info in rt.Keys)
            {
                this.PresentACLPrincipal(info.Owner);
            }

            return rt;

        }















        // operations on objects

        public void DeleteObject(string bucket, string key)
        {
            ClientResponse response = this.HandleRedirects(bucket, key, delegate(S3Client item0, RequestStyle item1)
            {
                return item0.DeleteObject(item1, bucket, key);
            });

            if (response.HttpStatus.Equals(HttpStatus.NoContent))
            {
                // expected
                return;
            }

            throw Unexpected(response);
        }

        public NameValuePairs GetObjectHeaders(string bucket, string key)
        {
            ClientResponse response = this.HandleRedirects(bucket, key, delegate(S3Client item0, RequestStyle item1)
            {
                return item0.HeadObject(item1, bucket, key);
            });
          
            if (response.HttpStatus.Equals(HttpStatus.OK))
            {
                return new NameValuePairs(response.Headers);
            }

            throw Unexpected(response);
        }

        public byte[] GetObjectAsBytes(string bucket, string key)
        {
            using (MemoryStream outputStream = new MemoryStream())
            {
                using (Stream inputStream = this.GetObjectAsStream(bucket, key, true))
                {
                    Utils.BufferedTransfer(inputStream, outputStream);
                }
                return outputStream.ToArray();
            }
        }

        public void GetObjectAsFile(string bucket, string key, string path)
        {
            using (Stream inputStream = this.GetObjectAsStream(bucket, key, true))
            {
                using (Stream outputStream = File.Create(path))
                {
                    Utils.BufferedTransfer(inputStream, outputStream);
                }
            }
        }

        public string GetObjectAsString(string bucket, string key)
        {
            using (StreamReader rt = new StreamReader(this.GetObjectAsStream(bucket, key, true)))
            {
                return rt.ReadToEnd();
            }
        }

        public Stream GetObjectAsStream(string bucket, string key, bool bufferOutput)
        {
            ClientResponse response = this.HandleRedirects(bucket, key, delegate(S3Client item0, RequestStyle item1)
            {
                return item0.GetObject(item1, bucket, key, bufferOutput);
            });

            if (response.HttpStatus.Equals(HttpStatus.OK))
            {
                return response.AsStream();
            }

            throw Unexpected(response);
        }

        public void PutObjectAsString(string bucket, string key, string text)
        {
            this.PutObjectAsBytes(bucket, key, new NameValuePairs(), null, Encoding.UTF8.GetBytes(text));
        }

        public void PutObjectAsBytes(string bucket, string key, NameValuePairs headers, Action<long> bytesTransferred, byte[] bytes)
        {
            using (MemoryStream stream = new MemoryStream(bytes))
            {
                this.PutObjectAsStream(bucket, key, headers, bytesTransferred, stream, bytes.Length);
            }
        }

        public void PutObjectAsFile(string bucket, string key, NameValuePairs headers, Action<long> bytesTransferred, string path)
        {
            using (Stream stream = File.OpenRead(path))
            {
                this.PutObjectAsStream(bucket, key, headers, bytesTransferred, stream, new FileInfo(path).Length);
            }
        }

        public void PutObjectAsStream(string bucket, string key, NameValuePairs headers, Action<long> bytesTransferred, Stream input, long length)
        {
            headers = new NameValuePairs(headers);  // COPY!

            headers.SetIfNotExists(S3Constants.Headers.CannedAccessPolicy, S3Constants.CannedAccessPolicies.Private);
            headers.SetIfNotExists(HttpHeader.ContentType, MimeMap.Get(key));

            // make sure the canned acl is valid
            string cannedACL = headers[S3Constants.Headers.CannedAccessPolicy];
            if (!Utils.Exists(S3Constants.CannedAccessPolicies.All, cannedACL) )
            {
                throw CodePlexException.Format("Invalid canned ACL '{0}', must be one of: {1}", cannedACL, Utils.Join(", ",S3Constants.CannedAccessPolicies.All));
            }

            // first send fake requests
            S3Client client = _client;
            RequestStyle style = RequestStyle.Path;

            ClientResponse response = this.HandleRedirects(bucket, key, delegate(S3Client item0, RequestStyle item1)
            {
              client = item0;
              style = item1;

              string fakeContent = " "; // ff needs at least one byte
              NameValuePairs fakeHeaders = new NameValuePairs();
              fakeHeaders[HttpHeader.ContentLength] = fakeContent.Length.ToString();
              return item0.PutObject(item1, bucket, key, fakeHeaders, Utils.ToStream(fakeContent), bytesTransferred);
            });


            // finally send the real request
            if (response.HttpStatus.Equals(HttpStatus.OK))
            {
                headers[HttpHeader.ContentLength] = length.ToString();
                response = client.PutObject(style, bucket, key, headers, input, bytesTransferred);

                if (response.HttpStatus.Equals(HttpStatus.OK))
                {
                    // expected 
                    return;
                }
            }

            throw Unexpected(response);
        }





































        private ClientResponse HandleRedirects(string bucket, string key, Fn<S3Client, RequestStyle, ClientResponse> fn)
        {
            ClientResponse response = null;

            Action<string> call = delegate(string item)
           {
               S3Client client = new S3Client(_client, new Uri(_client.RootUri.Scheme + "://" + item + "/"));
               client.Tracer.AddListener(this.Tracer);
               response = fn(client, RequestStyle.VHost);
           };


            // Call #1:  old "path-style" request 
            // try to skip this call if request-style pref = "vhost"
            string vhostEndpoint = bucket + "." + S3Constants.Host;
            if (
                _requestStylePreference == RequestStyle.Path                    // user doesn't care about (or does not support!) vhost-style calls
                || !this.IsBucketValid(bucket, BucketNaming.ValidDNS)           // bucket name does not conform to amazon's naming rules
                || !Utils.CheckHostName(vhostEndpoint)                          // bucket name does not conform to .net's (Uri) naming rules
                || key == ".."                                                  // known not to work using vhost-style, see http://developer.amazonwebservices.com/connect/thread.jspa?threadID=18596
                )
            {
                response = fn(_client, RequestStyle.Path);
            }


            // Call #2:  new "vhost-style" request
            // either call #1 was not attempted or we got the 301
            if (response==null || response.HttpStatus.Equals(HttpStatus.MovedPermanently))
            {
                call(vhostEndpoint);
            }

            // Call #3:  handle the 307 redirect if necessary
            if (response.HttpStatus.Equals(HttpStatus.TemporaryRedirect))
            {
                string temporaryEndpoint = new Uri(response.Headers[HttpHeader.Location]).Host;
                call(temporaryEndpoint);
            }

            return response;
        }


        private S3Exception Unexpected(ClientResponse response)
        {
            HttpStatus status = response.HttpStatus;

            XmlReader reader = response.AsXmlReader();

            string message = "TODO";

            NameValuePairs fields = new NameValuePairs();

            while (reader.Read())
            {
                if (reader.NodeType == XmlNodeType.Element && reader.Name != "Error")
                {
                    fields[reader.Name] = Utils.ReadString(reader);
                }
            }

            message = fields["Message"];

            if (string.IsNullOrEmpty(message))
            {
                if (fields["Code"] == "InvalidArgument")
                {
                    message = string.Format("InvalidArgument: '{0}'='{1}'", fields["ArgumentName"], fields["ArgumentValue"]);
                }
            }

            fields.Add("Status", status.ToString());
            return S3Exception.Format(fields, null, message);
        }

        private void PresentACL(ACL acl)
        {
            this.PresentACLPrincipal(acl.Owner);

            foreach (ACLPrincipal principal in acl.Principals)
            {
                this.PresentACLPrincipal(principal);
            }
        }

        private void PresentACLPrincipal(ACLPrincipal principal)
        {
            if (!_knownPrincipals.Contains(principal))
            {
                _knownPrincipals.Add(principal);
            }
        }

        private ListBucketResult InternalListBucket(string bucket, string prefix, string delimiter)
        {
            int amazonReturns = 1000;

            List<Key> infos = new List<Key>();
            List<string> commonPrefixes = new List<string>();
            string marker = null;
            InternalListBucketResult result = null;
            do
            {
                result = this.InternalListBucketCall(bucket, prefix, marker, amazonReturns, delimiter);

                infos.AddRange(result.Objects);
                commonPrefixes.AddRange(result.CommonPrefixes);

                if (infos.Count > 0)
                {
                    marker = infos[infos.Count - 1].Name;
                }


            } while (result.IsTruncated);

            return new ListBucketResult(infos, commonPrefixes);
        }

        private InternalListBucketResult InternalListBucketCall(string bucket, string prefix, string marker, int maxKeys, string delimiter)
        {
            NameValuePairs nvp = new NameValuePairs();

            if (prefix != null)
            {
                nvp["prefix"] = prefix;
            }
            if (marker != null)
            {
                nvp["marker"] = marker;
            }
            if (maxKeys != 0)
            {
                nvp["max-keys"] = maxKeys.ToString();
            }
            if (delimiter != null)
            {
                nvp["delimiter"] = delimiter;
            }

            ClientResponse response = this.HandleRedirects(bucket, null, delegate(S3Client item0, RequestStyle item1)
            {
                return item0.GetBucket(item1, bucket, nvp);
            });

            if (response.HttpStatus.Equals(HttpStatus.OK))
            {

                // unfortunately we need to preprocess the bucket listing due to the /r/n issue
                string xml = response.AsString();

                xml = Regex.Replace(xml, "<Key>(.*?)</Key>", delegate(Match item)
                {
                    string fixedKey = item.Groups[1].Value
                        .Replace("\r", Utils.ToHexXmlCharacterEntity('\r'))
                        .Replace("\n", Utils.ToHexXmlCharacterEntity('\n'));

                    return "<Key>" + fixedKey + "</Key>";
                },RegexOptions.Singleline);

                XmlReaderSettings settings = new XmlReaderSettings();
                settings.CheckCharacters = false;

                using (XmlReader reader = XmlReader.Create(Utils.ToStream(xml), settings))
                {
                    return InternalListBucketResult.Parse(reader);
                }   
            }

            throw this.Unexpected(response);
        }

        

       















        // acl-related

        public ACL GetACL(string bucket)
        {
            return this.GetACL(bucket, null);
        }

        public ACL GetACL(string bucket, string key)
        {
            Converter<XmlReader,ACL> builder = delegate(XmlReader reader)
            {
                ACL rt = ParseACL(reader, GetPermissionDescription);

                this.PresentACL(rt);
                return rt;
            };

            ClientResponse response = this.HandleRedirects(bucket, key, delegate(S3Client item0, RequestStyle item1)
            {
                return item0.GetAcl(item1, bucket, key);
            });
                
            if (response.HttpStatus.Equals(HttpStatus.OK))
            {
                return builder(response.AsXmlReader());
            }

            throw Unexpected(response);

        }


        public void SetACL(string bucket, ACL acl)
        {
            this.SetACL(bucket, null, acl);
        }
        public void SetACL(string bucket, string key, ACL acl)
        {
            /*
             
            PUT /quotes/Neo?acl HTTP/1.0
            Content-Length: 214
            Host: s3.amazonaws.com
            Authorization: AWS 15B4D3461F177624206A:xQE0diMbLRepdf3YB+FIc8F2Cy8=
            Date: Thu, 17 Nov 2005 07:48:33 GMT
            Content-Type: text/plain

            <AccessControlPolicy>
              <Owner>
                <ID>a9a7b886d6fd24a52fe8ca5bef65f89a64e0193f23000e241bf9b1c61be666e9</ID>
                <DisplayName>chriscustomer</DisplayName>
              </Owner>
              <AccessControlList>
                <Grant>
                  <Grantee xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="CanonicalUser">
                    <ID>a9a7b886d6fd24a52fe8ca5bef65f89a64e0193f23000e241bf9b1c61be666e9</ID>
                    <DisplayName>chriscustomer</DisplayName>
                  </Grantee>
                  <Permission>FULL_CONTROL</Permission>
                </Grant>
              </AccessControlList>
            <AccessControlPolicy>
             * 
             
 <Grantee xsi:type="AmazonCustomerByEmail">
  <EmailAddress>chriscustomer@email.com</EmailAddress>
</Grantee>
             * 
            */

            using (MemoryStream stream = new MemoryStream())
            {

                XmlWriter aclWriter = XmlWriter.Create(stream);

                aclWriter.WriteStartDocument();
                aclWriter.WriteStartElement("AccessControlPolicy", S3Constants.NamespaceURI);

                aclWriter.WriteStartElement("Owner");
                aclWriter.WriteElementString("ID", acl.Owner.ID);
                aclWriter.WriteElementString("DisplayName", acl.Owner.Name);

                aclWriter.WriteEndElement(); // Owner

                aclWriter.WriteStartElement("AccessControlList");

                foreach (ACLPrincipal principal in acl.Principals)
                {

                    foreach (string permission in acl.GetPermissions(principal))
                    {
                        aclWriter.WriteStartElement("Grant");

                        aclWriter.WriteStartElement("Grantee");

                        if (principal.IsGroup)
                        {
                            aclWriter.WriteAttributeString("xsi", "type", S3Constants.XMLSchemaURI, "Group");
                            aclWriter.WriteElementString("URI", principal.ID);
                        }
                        else
                        {
                            AmazonCustomerByEmail email = principal as AmazonCustomerByEmail;
                            if (email != null)
                            {
                                aclWriter.WriteAttributeString("xsi", "type", S3Constants.XMLSchemaURI, "AmazonCustomerByEmail");
                                aclWriter.WriteElementString("EmailAddress", email.EmailAddress);
                            }
                            else
                            {
                                aclWriter.WriteAttributeString("xsi", "type", S3Constants.XMLSchemaURI, "CanonicalUser");

                                aclWriter.WriteElementString("ID", principal.ID);
                                //  aclWriter.WriteElementString("DisplayName", principal.Name);
                            }
                        }


                        aclWriter.WriteEndElement(); // Grantee

                        aclWriter.WriteElementString("Permission", permission);

                        aclWriter.WriteEndElement(); // Grant
                    }
                }


                aclWriter.WriteEndElement(); // AccessControlList



                aclWriter.WriteEndElement(); // AccessControlPolicy

                aclWriter.WriteEndDocument();

                aclWriter.Flush();

                ClientResponse response = this.HandleRedirects(bucket, key, delegate(S3Client item0, RequestStyle item1)
                {
                    // don't write the preamble!
                    stream.Position = aclWriter.Settings.Encoding.GetPreamble().Length;

                    if (string.IsNullOrEmpty(key))
                    {
                        return item0.PutACL(item1, bucket, stream);
                    }
                    else
                    {
                        return item0.PutACL(item1, bucket, key, stream);
                    }
                });
            

                if (response.HttpStatus.Equals(HttpStatus.OK))
                {
                    // expected
                    return;
                }

                throw Unexpected(response);
            }



        }

        public ReadOnlyList<ACLPrincipal> GetPrincipals()
        {
            return new ReadOnlyList<ACLPrincipal>(_knownPrincipals);
        }

        public ACL NewACL()
        {
            ACL rt = new ACL(false, this.Owner, this.GetPermissionDescription);
            return rt;
        }


        private string GetPermissionDescription(string permission)
        {

            NameValuePairs descriptions = new NameValuePairs();
            descriptions.Add(S3Constants.Permissions.Read, "When applied to a bucket, this grants permission to list the bucket. When applied to an object, this grants permission to read the object data and/or metadata.");
            descriptions.Add(S3Constants.Permissions.Write, "When applied to a bucket, this grants permission to create, overwrite, and delete any object in the bucket. This permission is not supported for objects (it is reserved for future use).");
            descriptions.Add(S3Constants.Permissions.ReadAcp, "Grants permission to read the ACL for the applicable bucket or object. The owner of a bucket or object always has this permission implicitly.");
            descriptions.Add(S3Constants.Permissions.WriteAcp, "Gives permission to overwrite the ACL for the applicable bucket or object. The owner of a bucket or object always has this permission implicitly. Note that granting this permission is equivalent to granting FULL_CONTROL, because the grant recipient can now make any whatever changes to the ACL he or she likes!");
            descriptions.Add(S3Constants.Permissions.FullControl, "This permission is short-hand for the union of READ, WRITE, READ_ACP, and WRITE_ACP permissions. It does not convey additional rights, and is provide only for convenience.");

            return descriptions[permission];
        }

        public static ACL ParseACL(XmlReader reader, Converter<string, string> descriptions)
        {
            /*
            <AccessControlPolicy>
          <Owner>
            <ID>a9a7b886d6fd24a52fe8ca5bef65f89a64e0193f23000e241bf9b1c61be666e9</ID>
            <DisplayName>chriscustomer</DisplayName>
          </Owner>
          <AccessControlList>
            <Grant>
              <Grantee xsi:type="CanonicalUser">
                <ID>a9a7b886d6fd24a52fe8ca5bef65f89a64e0193f23000e241bf9b1c61be666e9</ID>
                <DisplayName>chriscustomer</DisplayName>
              </Grantee>
              <Permission>FULL_CONTROL</Permission>
            </Grant>
          </AccessControlList>
        <AccessControlPolicy>
             */

            ACL rt = null;

            ACLPrincipal currentGrantee = null;
            string currentPermission = null;

            while (reader.Read())
            {
                if (reader.NodeType == XmlNodeType.Element)
                {
                    if (reader.Name == "Owner")
                    {
                        ACLPrincipal owner = ParseACLPrincipal(reader, "Owner");
                        rt = new ACL(false,owner, descriptions);
                    }

                    if (reader.Name == "Grantee")
                    {
                        currentGrantee = ParseACLPrincipal(reader, "Grantee");
                    }

                    if (reader.Name == "Permission")
                    {
                        currentPermission = Utils.ReadString(reader);
                        rt.Add(currentGrantee, currentPermission);
                    }
                }

                if (reader.NodeType == XmlNodeType.EndElement && reader.Name == "AccessControlPolicy")
                {
                    return rt;
                }

            }

            throw CodePlexException.Format("Failed to parse acl.");



        }

        public static ACLPrincipal ParseACLPrincipal(XmlReader reader, string elementName)
        {
            bool isGroup = false;
            string currentID = null;
            string currentName = null;

            while (reader.Read())
            {
                if (reader.NodeType == XmlNodeType.Element)
                {
                    if (reader.Name == "ID")
                    {
                        currentID = Utils.ReadString(reader);
                    }
                    else if (reader.Name == "DisplayName")
                    {
                        currentName = Utils.ReadString(reader);
                      
                    }
                    else if (reader.Name == "URI")
                    {
                        currentID = Utils.ReadString(reader);
                        string[] tokens = currentID.Split('/');
                        currentName = tokens[tokens.Length - 1];
                        isGroup = true;
                    }
                }

                if (reader.NodeType == XmlNodeType.EndElement && reader.Name == elementName)
                {
                    return new ACLPrincipal(currentID, currentName, isGroup);
                }

            }

            throw CodePlexException.Format("Failed to parse acl principal.");

        }








        // bucket logging

        public void SetLoggingStatus(string bucket, BucketLoggingStatus status)
        {
            if (!this.ListBuckets().Buckets.Exists(bucket))
            {
                throw CodePlexException.Format("Bucket must exist: {0}", bucket);
            }

            //Log files will be written to the target bucket under the identity of a member of the http://acs.amazonaws.com/groups/s3/LogDelivery group. 
            //These writes are subject to the usual access control restrictions. Therefore, logs will not be delivered unless the 
            //access control policy of the target bucket grants the log delivery group WRITE access. To ensure log files are delivered 
            //correctly, the log delivery group must also have READ_ACP permission on the target bucket. 
            
            if (status.IsEnabled)
            {

                Datacenter bucketDatacenter = this.GetDatacenter(bucket);
                if (this.ListBuckets().Buckets.Exists(status.TargetBucket))
                {
                    Datacenter targetDatacenter = this.GetDatacenter(status.TargetBucket);
                    if (bucketDatacenter != targetDatacenter)
                    {
                        throw CodePlexException.Format("Logging target bucket for '{0}' must exist in the same datacenter '{1}'.", bucket, bucketDatacenter);
                    }
                }
                else
                {
                    this.CreateBucket(status.TargetBucket, bucketDatacenter);
                }


                ACLPrincipal logPrincipal = S3Constants.Principals.LogDelivery;
                ACL acl = this.GetACL(status.TargetBucket);

                bool changed = false;
                ReadOnlyList<string> existingPermissions = acl.GetPermissions(logPrincipal);

                Action<string> fn = delegate(string item)
                {
                    if (!existingPermissions.Contains(item))
                    {
                        acl.Add(logPrincipal, item);
                        changed = true;
                    }
                };

                fn(S3Constants.Permissions.Write);
                fn(S3Constants.Permissions.ReadAcp);
                

                if (changed)
                {
                    this.SetACL(status.TargetBucket, acl);
                }

            }



//<?xml version="1.0" encoding="UTF-8"?>
//<BucketLoggingStatus xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
//    <LoggingEnabled>
//        <TargetBucket>mylogs</TargetBucket>
//        <TargetPrefix>access_log-</TargetPrefix>
//    </LoggingEnabled>
//</BucketLoggingStatus>

            using (MemoryStream stream = new MemoryStream())
            {

                XmlWriter writer = XmlWriter.Create(stream);

                writer.WriteStartDocument();
                writer.WriteStartElement("BucketLoggingStatus", S3Constants.NamespaceURI);

                if (status.IsEnabled)
                {
                    writer.WriteStartElement("LoggingEnabled");
                    writer.WriteElementString("TargetBucket", status.TargetBucket);
                    writer.WriteElementString("TargetPrefix", status.TargetPrefix);
                    writer.WriteEndElement(); // LoggingEnabled
                }

                writer.WriteEndElement(); // BucketLoggingStatus



                writer.WriteEndDocument();

                writer.Flush();


                ClientResponse response = this.HandleRedirects(bucket, null, delegate(S3Client item0, RequestStyle item1)
                {
                    // don't write the preamble!
                    stream.Position = writer.Settings.Encoding.GetPreamble().Length;

                    return item0.PutLoggingStatus(item1, bucket, stream);
                });


                if (response.HttpStatus.Equals(HttpStatus.OK))
                {
                    return;
                }

                throw this.Unexpected(response);
            }
        }



        public IEnumerable<LogRecord> GetLogRecords(string bucket, string prefix, DateTime? from, DateTime? to)
        {

            foreach (Key key in this.ListBucket(bucket, prefix).Sort(delegate(Key lhs, Key rhs)
            {
                return lhs.Name.CompareTo(rhs.Name);
            }))
            {
                string filename = key.Name.Substring(prefix.Length);

                // 2007-01-02-03-15-59-79F6FA9C4977AC60
                DateTime? logDate = this.ParseLogDate(filename);

                if (
                    logDate != null 
                    &&(from==null||logDate>from)
                    &&(to==null||logDate<to)
                    )
                {
                    using (Stream inputStream = this.GetObjectAsStream(bucket, key.Name, false))
                    {
                        foreach (string record in Utils.Lines(inputStream))
                        {
                            yield return LogRecord.Parse(record);
                        }
                    }
                }


            }

        }

        private DateTime? ParseLogDate(string name)
        {
            Match m = Regex.Match(name, @"(^\d{4}-\d{2}-\d{2})-(\d{2}-\d{2}-\d{2})-\w{16}$");

            if (!m.Success)
            {
                return null;
            }

            string date = m.Groups[1].Value;
            string time = m.Groups[2].Value.Replace('-', ':');

            return DateTime.Parse(date + " " + time);
        }



        public BucketLoggingStatus GetLoggingStatus(string bucket)
        {

            ClientResponse response = this.HandleRedirects(bucket, null, delegate(S3Client item0, RequestStyle item1)
         {
             return item0.GetLoggingStatus(item1, bucket);
         });


            if (response.HttpStatus.Equals(HttpStatus.OK))
            {
                return BucketLoggingStatus.Parse(response.AsXmlReader());
            }

            throw Unexpected(response);
        }

















        // datacenter

        public Datacenter GetDatacenter(string bucket)
        {
            ClientResponse response = _client.GetLocation(RequestStyle.Path, bucket);

            if (response.HttpStatus.Equals(HttpStatus.OK))
            {
                string rt = ParseLocationConstraint(response.AsXmlReader());

                return rt == "EU" ? Datacenter.EU : Datacenter.US;
            }

            throw Unexpected(response);
        }

        private static string ParseLocationConstraint(XmlReader reader)
        {
            while (reader.Read())
            {
                if (reader.NodeType == XmlNodeType.Element)
                {
                    if (reader.Name == "LocationConstraint")
                    {
                        return Utils.ReadString(reader);
                    }
                }
            }

            throw CodePlexException.Format("Failed to parse location-constraint.");

        }

      
    }
}
